import 'package:flutter/widgets.dart';

/// Blocks a set of intents from launching [Action]s.
///
/// An [IntentBlocker] blocks [Action]s from running by stopping the associated
/// [Intent]s from bubbling any further up the widget tree.
///
/// Flutter includes default [Action]s at app-level that might not be desirable for
/// all applications.
///
/// To prevent [Intent]s from bubbling up to the default [Action]s, locate any widget
/// below the [Actions] widget that handles the desired [Intent]s (typically [MaterialApp]
/// or [WidgetsApp]), and wrap it with an [IntentBlocker] widget, passing the types
/// of the [Intent]s that should be blocked into the [intents] parameter.
///
/// For example, the following code blocks [ScrollIntent]s from bubbling up:
///
///     IntentBlocker(
///       intents: { ScrollIntent },
///       child: child,
///     )
class IntentBlocker extends StatelessWidget {
  const IntentBlocker({
    super.key,
    required this.intents,
    required this.child,
  });

  /// The types of intents that should be blocked.
  final Set<Type> intents;

  /// The rest of the widget tree.
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final actions = <Type, Action<Intent>>{};
    for (final intent in intents) {
      actions[intent] = DoNothingAction(consumesKey: false);
    }

    return Actions(
      actions: {
        ...actions,
        // Flutter might dispatch Intents individually or as a group. We want
        // to also block any desired Intents when they are inside a group.
        PrioritizedIntents: _BlockIntentInsideGroupAction(
          intents: intents,
        )
      },
      child: child,
    );
  }
}

/// A set of [Intent]s, which Flutter dispatches by default on non-Apple
/// platforms (Android, Windows, Linux), that should have its associated
/// [Action]s blocked.
///
/// {@template flutter_default_actions}
/// For example: The user presses the SPACE key on web. By default
/// Flutter emits an `ActivateIntent` and a `ScrollIntent`. But you
/// probably want to insert a " " in some text instead of activating
/// or scrolling. Those default Flutter intents must be blocked for
/// two reasons. First, you don't want to scroll down every time you
/// type a space. Second, the SPACE key event won't have a chance to
/// be handled by the OS IME if Flutter handles the key with an [Action].
/// Therefore, you should preveng this set of [Intent]s from bubbling up,
/// to block those default Flutter behaviors, let the IME handle the key event,
/// and enjoy expected text input.
/// {@endtemplate}
///
/// To prevent the default Flutter [Action]s, locate the specific widget
/// where you want to run non-default behaviors, such as inserting a space
/// instead of scrolling. Wrap that widget with an [IntentBlocker] widget, and
/// then pass this set of [Intent] types to block the default behaviors:
///
///     IntentBlocker(
///       intents: nonAppleBlockedIntents,
///       child: SuperEditor(),
///     )
///
/// See [WidgetsApp.defaultShortcuts] for the list of keybindings that Flutter
/// adds by default.
final Set<Type> nonAppleBlockedIntents = {
  ActivateIntent,
  ScrollIntent,
};

/// A set of [Intent]s, which Flutter dispatches by default on Apple
/// platforms (macOS and iOS), that should have its associated
/// [Action]s blocked.
///
/// {@macro flutter_default_actions}
///
/// To prevent the default Flutter [Action]s, locate the specific widget
/// where you want to run non-default behaviors, such as inserting a space
/// instead of scrolling. Wrap that widget with an [IntentBlocker] widget, and
/// then pass this set of [Intent] types to block the default behaviors:
///
///     IntentBlocker(
///       intents: appleBlockedIntents,
///       child: SuperEditor(),
///     )
///
/// See [WidgetsApp.defaultShortcuts] for the list of keybindings that Flutter
/// adds by default.
final Set<Type> appleBlockedIntents = {
  // Generated by pressing LEFT/RIGHT ARROW.
  ExtendSelectionByCharacterIntent,
  // Generated by pressing UP/DOWN ARROW.
  ExtendSelectionVerticallyToAdjacentLineIntent,
  // Generated by pressing PAGE UP/DOWN.
  ScrollIntent,
  // Generated by pressing HOME/END.
  ScrollToDocumentBoundaryIntent,
  // Generated by pressing TAB.
  NextFocusIntent,
  // Generated by pressing SHIFT + TAB.
  PreviousFocusIntent,
  // Generated by pressing SPACE.
  ActivateIntent,
};

/// An [Action], which blocks certain [Intent]s dispatched inside a group of [Intent]s
/// from launching other [Action]s.
///
/// A [_BlockIntentInsideGroupAction] blocks other [Action]s from running by stopping
/// the associated [Intent]s from bubbling any further up the widget tree.
///
/// To prevent [Intent]s inside of a group from bubbling up to the default [Action]s, locate any widget
/// below the [Actions] widget that handles the desired [Intent]s (typically [MaterialApp]
/// or [WidgetsApp]), and wrap it with an [Actions] widget. Then, associate [PrioritizedIntents]
/// with [_BlockIntentInsideGroupAction] pass the set of [Intent]s that should be blocked.
///
/// For example, the following code blocks [ScrollIntent]s from bubbling up:
///
///     Actions(
///       actions: {
///         PrioritizedIntents: _BlockIntentInsideGroupAction(
///           intents: { ScrollIntent },
///         )
///       },
///       child: child,
///     );
class _BlockIntentInsideGroupAction extends Action<PrioritizedIntents> {
  _BlockIntentInsideGroupAction({
    required this.intents,
  });

  final Set<Type> intents;

  @override
  bool consumesKey(PrioritizedIntents intent) => false;

  @override
  void invoke(PrioritizedIntents intent) {}

  @override
  bool isEnabled(PrioritizedIntents intent, [BuildContext? context]) {
    final FocusNode? focus = primaryFocus;
    if (focus == null || focus.context == null) {
      return false;
    }

    for (final candidateIntent in intent.orderedIntents) {
      if (_hasEnabledAction(candidateIntent, context)) {
        // Flutter wants to run an Action for this intent.
        if (intents.contains(candidateIntent.runtimeType)) {
          // We want to prevent this intent from bubbling up. Return `true` to
          // signal to Flutter that we want to handle it.
          return true;
        }

        // We don't care about the intent that is going to have its corresponding
        // Action executed. Let it bubble up so Flutter will execute it.
        return false;
      }
    }

    // We didn't find any intents with a corresponding enabled Action. Let the
    // intent bubble up.
    return false;
  }

  bool _hasEnabledAction(Intent intent, BuildContext? context) {
    final Action<Intent>? candidateAction = Actions.maybeFind<Intent>(
      primaryFocus!.context!,
      intent: intent,
    );

    if (candidateAction == null) {
      // We didn't find an Action associated with the given intent.
      return false;
    }

    return (candidateAction is ContextAction<Intent>)
        ? candidateAction.isEnabled(intent, context)
        : candidateAction.isEnabled(intent);
  }
}
